//
// Created by tang on 2022/10/24.
//

#include "program.hpp"
#include <utils/path_utils.hpp>
#include <glm/gtc/type_ptr.hpp>

namespace Shaders {

    const char *ShaderType(GLenum shader_type) {
        switch (shader_type) {
            case GL_VERTEX_SHADER:
                return "Vertex Shader";
            case GL_FRAGMENT_SHADER:
                return "Fragment Shader";
            default:
                return "Unknown Shader Type";
        }
    }

    std::string ReadShaderSource(const std::string &filename) {
        const auto full_path = std::filesystem::path(Utils::GetShadersDirPath()) / filename;
        std::ifstream ifs(full_path, std::ios::binary | std::ios::in);
        std::string code;
        if (ifs.is_open()) {
            ifs.seekg(0, std::ios::end);
            auto const length = ifs.tellg();
            ifs.seekg(0);
            code.resize(length);
            ifs.read(&code[0], length);
            ifs.close();
        } else
            LOG_F("Unable to open file \"%s\"", full_path.c_str());
        return code;
    }

    std::tuple<bool, GLuint> CompileShader(const std::string &code, GLenum type) {
        GLuint shader;
        GLCallValidation(shader = glCreateShader(type));
        const char *pCode = code.data();
        const GLint length = code.length();
        GLCallValidation(glShaderSource(shader, 1, &pCode, &length));
        GLCallValidation(glCompileShader(shader));

        // Check
        GLint success;
        GLchar infoLog[1024]{ 0 };
        GLCallValidation(glGetShaderiv(shader, GL_COMPILE_STATUS, &success));

        if (!success) {
            GLsizei log_length;
            GLCallValidation(glGetShaderInfoLog(shader, std::size(infoLog), &log_length, infoLog));

            LOG_F("Shader Compile Failed! type: %s, Error: %s, source: %s", ShaderType(type), infoLog, pCode);

            GLCallValidation(glDeleteShader(shader));
            return std::make_tuple(false, static_cast<GLuint>(-1));
        }

        return std::make_tuple(true, shader);
    }
}

namespace GL {
    Program::Program(const std::string &name) {
        GLCallValidation(m_program = glCreateProgram());

        std::vector<GLuint> shaders;
        shaders.reserve(2);

        // vertex shader
        {
            const auto vs_filename = name + ".vert.glsl";
            auto const [compiled, shader] =
                    Shaders::CompileShader(Shaders::ReadShaderSource(vs_filename), GL_VERTEX_SHADER);
            if (compiled) {
                GLCallValidation(glAttachShader(m_program, shader));
                shaders.emplace_back(shader);
            }
        }

        // fragment shader
        {
            const auto fs_filename = name + ".frag.glsl";
            auto const [compiled, shader] =
                    Shaders::CompileShader(Shaders::ReadShaderSource(fs_filename), GL_FRAGMENT_SHADER);
            if (compiled) {
                GLCallValidation(glAttachShader(m_program, shader));
                shaders.emplace_back(shader);
            }
        }

        GLCallValidation(glLinkProgram(m_program));
        for (auto const x: shaders) {
            GLCallValidation(glDeleteShader(x));
        }

        // Check
        GLint success;
        GLchar infoLog[1024]{ 0 };
        GLCallValidation(glGetProgramiv(m_program, GL_LINK_STATUS, &success));

        if (!success) {
            GLsizei log_length;
            GLCallValidation(glGetProgramInfoLog(m_program, std::size(infoLog), &log_length, infoLog));

            LOG_F("Program Link Error: %s", infoLog);

            GLCallValidation(glDeleteProgram(m_program));
            m_program = 0;
        }
    }

    Program::~Program() {
        NotUse();
        GLCallValidation(glDeleteProgram(m_program));
    }

    bool Program::Valid() const {
        return m_program != 0;
    }

    void Program::Use() const {
        assert(Valid());
        GLCallValidation(glUseProgram(m_program));
    }

    void Program::NotUse() const {
        GLCallValidation(glUseProgram(0));
    }

    void Program::UniformMat4f(const char *name, const glm::mat4 &value) {
        GLCallValidation(glUniformMatrix4fv(FindUniformLocation(name), 1, GL_FALSE, value_ptr(value)));
    }

    void Program::Uniform4f(const char *name, const glm::vec4 &value) {
        GLCallValidation(glUniform4fv(FindUniformLocation(name), 1, value_ptr(value)));
    }

    void Program::Uniform3f(const char *name, const glm::vec3 &value) {
        GLCallValidation(glUniform3fv(FindUniformLocation(name), 1, value_ptr(value)));
    }

    void Program::Uniform2f(const char *name, const glm::vec2 &value) {
        GLCallValidation(glUniform2fv(FindUniformLocation(name), 1, value_ptr(value)));
    }

    void Program::Uniform1f(const char *name, float value) {
        GLCallValidation(glUniform1fv(FindUniformLocation(name), 1, &value));
    }

    void Program::Uniform1i(const char *name, GLint value) {
        GLCallValidation(glUniform1iv(FindUniformLocation(name), 1, &value));
    }

    GLint Program::FindUniformLocation(const char *name) {
        assert(Valid());
        auto it = m_uniform_location_cache.find(name);
        if (it == end(m_uniform_location_cache)) {
            GLint location;
            GLCallValidation(location = glGetUniformLocation(m_program, name));
            if (location >= 0) {
                it = m_uniform_location_cache.emplace(name, location).first;
            } else
                LOG_E("Uniform \"%s\" not found!", name);
        }
        return it->second;
    }

    ProgramPtr Program::Create(const std::string &name) {
        return std::make_unique<Program>(name);
    }
}
